package com.dream.file;

import com.dream.file.chunk.ChunkUploadResult;
import com.dream.file.chunk.UploadChunkRepository;
import com.dream.file.chunk.UploadTaskRepository;
import io.minio.*;
import io.minio.messages.Tags;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * MinIO文件存储实现
 * 基于MinIO对象存储服务实现文件存储、上传、下载等功能
 */
@Slf4j
public class MinioFileStorage implements FileStorage {

    /**
     * MinIO客户端
     */
    private final MinioClient client;
    
    /**
     * 存储配置信息
     */
    private final StorageConfig config;
    
    /**
     * 上传任务仓库
     */
    private final UploadTaskRepository uploadTaskRepository;
    
    /**
     * 上传分片仓库
     */
    private final UploadChunkRepository uploadChunkRepository;
    
    /**
     * 线程池，用于异步处理任务
     */
    private final ExecutorService executorService;

    /**
     * 构造方法
     * 
     * @param config 存储配置
     * @param uploadTaskRepository 上传任务仓库
     * @param uploadChunkRepository 上传分片仓库
     */
    public MinioFileStorage(StorageConfig config,
                            UploadTaskRepository uploadTaskRepository,
                            UploadChunkRepository uploadChunkRepository) {
        this.config = config;
        try {
            this.client = MinioClient.builder()
                    .endpoint(config.getEndpoint())
                    .credentials(config.getAssessKey(), config.getSecretKey())
                    .build();
            createBucketIfNotExists();
        } catch (Exception e) {
            throw new FileStorageException("Init minio client fail.", e);
        }
        this.uploadTaskRepository = uploadTaskRepository;
        this.uploadChunkRepository = uploadChunkRepository;
        this.executorService = Executors.newFixedThreadPool(10);
    }

    /**
     * 如果存储桶不存在则创建
     */
    private void createBucketIfNotExists() {
        try {
            boolean exists = client.bucketExists(BucketExistsArgs.builder()
                    .bucket(config.getBucketName())
                    .build());
            if (!exists) {
                client.makeBucket(MakeBucketArgs.builder()
                        .bucket(config.getBucketName())
                        .build());
                log.info("Create minio bucket: {} success.", config.getBucketName());
            }
        } catch (Exception e) {
            throw new FileStorageException("Create or check minio bucket fail.", e);
        }
    }

    /**
     * 上传文件
     * 
     * @param inputStream 文件输入流
     * @param path 存储路径
     * @param fileName 文件名
     * @return 访问URL
     */
    @Override
    public String upload(InputStream inputStream, String path, String fileName) {
        try {
            String objectName = buildObjectName(path, fileName);
            Map<String, String> headers = new HashMap<>();
            headers.put("Content-Type", getContentType(fileName));

            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(config.getBucketName())
                    .object(objectName)
                    .stream(inputStream, inputStream.available(), -1)
                    .headers(headers)
                    .build();

            client.putObject(args);
            log.info("upload file: {} to minio success.", objectName);
            return generateUrl(objectName);
        } catch (Exception e) {
            throw new FileStorageException("upload file fail.", e);
        }
    }

    /**
     * 分片上传文件
     * 支持大文件的分片上传，可以实现断点续传
     * 
     * @param inputStream 文件输入流
     * @param path 存储路径
     * @param fileName 文件名
     * @param chunkNumber 当前分片序号
     * @param totalChunks 总分片数
     * @return 分片上传结果
     */
    @Override
    public ChunkUploadResult uploadChunk(InputStream inputStream, String path, String fileName,
                                         int chunkNumber, int totalChunks) {
        /*
            实际项目中应该将uploadId和分片状态持久化存储（例如使用数据库）
            需要添加分片上传的超时处理机制
            建议添加定时任务清理未完成的分片文件
            可以添加断点续传功能，通过记录已上传的分片信息实现
            建议添加文件MD5校验，确保文件完整性
            可以实现并行上传分片以提高性能
        */
        try {
            String uploadId = getOrCreateMultipartUploadId(path, fileName);
            String objectName = buildObjectName(path, fileName);

            ComposeSource partSource = ComposeSource.builder()
                    .bucket(config.getBucketName())
                    .object(getChunkObjectName(objectName, uploadId, chunkNumber))
                    .build();

            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(config.getBucketName())
                    .object(partSource.object())
                    .stream(inputStream, inputStream.available(), -1)
                    .build();

            ObjectWriteResponse response = client.putObject(putObjectArgs);

            ChunkUploadResult result = new ChunkUploadResult();
            result.setUploadId(uploadId);
            result.setEtag(response.etag());
            result.setPartNumber(chunkNumber);
            result.setCompleted(chunkNumber == totalChunks);

            // 如果是最后一个分片，执行合并操作
            if (result.isCompleted()) {
                completeMultipartUpload(objectName, uploadId, totalChunks);
            }

            return result;
        } catch (Exception e) {
            throw new FileStorageException("chunk upload file fail.", e);
        }
    }

    /**
     * 下载文件
     * 
     * @param path 文件路径
     * @param fileName 文件名
     * @return 文件输入流
     */
    @Override
    public InputStream download(String path, String fileName) {
        try {
            String objectName = buildObjectName(path, fileName);
            GetObjectArgs args = GetObjectArgs.builder()
                    .bucket(config.getBucketName())
                    .object(objectName)
                    .build();

            return client.getObject(args);
        } catch (Exception e) {
            throw new FileStorageException("download file fail.", e);
        }
    }

    /**
     * 范围下载文件
     * 支持断点续传下载，指定下载文件的部分内容
     * 
     * @param path 文件路径
     * @param fileName 文件名
     * @param start 开始位置（字节偏移量）
     * @param end 结束位置（字节偏移量）
     * @return 文件部分内容的输入流
     */
    @Override
    public InputStream downloadRange(String path, String fileName, long start, long end) {
        try {
            String objectName = buildObjectName(path, fileName);
            GetObjectArgs args = GetObjectArgs.builder()
                    .bucket(config.getBucketName())
                    .object(objectName)
                    .offset(start)
                    .length(end - start + 1)
                    .build();

            return client.getObject(args);
        } catch (Exception e) {
            throw new FileStorageException("范围下载失败", e);
        }
    }

    /**
     * 删除文件
     * 
     * @param path 文件路径
     * @param fileName 文件名
     * @return 是否删除成功
     */
    @Override
    public boolean delete(String path, String fileName) {
        try {
            String objectName = buildObjectName(path, fileName);
            client.removeObject(RemoveObjectArgs.builder()
                    .bucket(config.getBucketName())
                    .object(objectName)
                    .build());
            log.info("文件删除成功: {}", objectName);
            return true;
        } catch (Exception e) {
            log.error("文件删除失败", e);
            return false;
        }
    }

    /**
     * 获取文件元数据信息
     * 
     * @param path 文件路径
     * @param fileName 文件名
     * @return 文件元数据
     */
    @Override
    public FileMetadata getMetadata(String path, String fileName) {
        try {
            String objectName = buildObjectName(path, fileName);
            StatObjectResponse stat = client.statObject(StatObjectArgs.builder()
                    .bucket(config.getBucketName())
                    .object(objectName)
                    .build());

            FileMetadata metadata = new FileMetadata();
            metadata.setFileName(fileName);
            metadata.setPath(path);
            metadata.setSize(stat.size());
            metadata.setContentType(stat.contentType());
            metadata.setLastModified(stat.lastModified().toInstant());
            metadata.setEtag(stat.etag());

            // 获取自定义元数据
            Tags tags = client.getObjectTags(GetObjectTagsArgs.builder()
                    .bucket(config.getBucketName())
                    .object(objectName)
                    .build());
            metadata.setMetadata(new HashMap<>(tags.get()));

            return metadata;
        } catch (Exception e) {
            throw new FileStorageException("获取文件元数据失败", e);
        }
    }

    // 工具方法
    /**
     * 构建对象名称
     * 将路径和文件名组合成MinIO中的对象名
     * 
     * @param path 文件路径
     * @param fileName 文件名
     * @return 完整的对象名
     */
    private String buildObjectName(String path, String fileName) {
        String basePath = StringUtils.trimToEmpty(config.getBasePath());
        path = StringUtils.trimToEmpty(path);
        return String.format("%s/%s/%s", basePath, path, fileName)
                .replaceAll("//+", "/")
                .replaceAll("^/", "");
    }

    /**
     * 生成访问URL
     * 
     * @param objectName 对象名称
     * @return 访问URL
     */
    private String generateUrl(String objectName) {
        return String.format("%s/%s/%s",
                config.getEndpoint(),
                config.getBucketName(),
                objectName);
    }

    /**
     * 获取文件内容类型
     * 
     * @param fileName 文件名
     * @return 内容类型/MIME类型
     */
    private String getContentType(String fileName) {
        // 简单实现，实际项目中可以使用更完善的方法
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        // from jdk14, 如果分支体只有一条语句，则无需再使用 break 语句，默认情况下不会贯穿（fall-through）到下一个 case
        return switch (ext) {
            case "jpg", "jpeg" -> "image/jpeg";
            case "png" -> "image/png";
            case "pdf" -> "application/pdf";
            default -> "application/octet-stream";
        };
    }

    private String getOrCreateMultipartUploadId(String path, String fileName) {
        // 在实际项目中，应该将uploadId持久化到数据库中
        // 这里简单使用UUID模拟
        return UUID.randomUUID().toString();
    }

    private String getChunkObjectName(String objectName, String uploadId, int chunkNumber) {
        return String.format("%s.%s.part%d", objectName, uploadId, chunkNumber);
    }

    private void completeMultipartUpload(String objectName, String uploadId, int totalChunks) {
        try {
            // 准备合并的源文件列表
            List<ComposeSource> sources = new ArrayList<>();
            for (int i = 1; i <= totalChunks; i++) {
                String chunkObjectName = getChunkObjectName(objectName, uploadId, i);
                ComposeSource source = ComposeSource.builder()
                        .bucket(config.getBucketName())
                        .object(chunkObjectName)
                        .build();
                sources.add(source);
            }

            // 合并文件
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                    .bucket(config.getBucketName())
                    .object(objectName)
                    .sources(sources)
                    .build();
            client.composeObject(composeObjectArgs);

            // 删除分片文件
            for (ComposeSource source : sources) {
                client.removeObject(RemoveObjectArgs.builder()
                        .bucket(config.getBucketName())
                        .object(source.object())
                        .build());
            }
        } catch (Exception e) {
            throw new FileStorageException("合并分片失败", e);
        }
    }
}
